const std = @import("std");
const builtin = @import("builtin");

const Dependence = struct {
    root_path: []const u8,
    inc_paths: []const []const u8 = &.{},
    lib_names: []const []const u8 = &.{},
};

const dependences = [_]Dependence{
    .{
        .root_path = "../awtk",
        .inc_paths = &.{ "src", "src/ext_widgets" },
    },
    .{
        .root_path = "../awtk-mvvm",
        .inc_paths = &.{"src"},
        .lib_names = &.{"mvvm"},
    },
};

fn isCrossBuild(target: std.Build.ResolvedTarget) bool {
    const arch = target.query.cpu_arch orelse builtin.cpu.arch;
    const os = target.query.os_tag orelse builtin.os.tag;
    const abi = target.query.abi orelse builtin.abi;
    return arch != builtin.cpu.arch or
        os != builtin.os.tag or
        abi != builtin.abi;
}

fn addDependence(b: *std.Build, module: *std.Build.Module, depend: Dependence) void {
    const module_path = b.path(depend.root_path);

    for (depend.inc_paths) |sub| {
        const inc_path = module_path.path(b, sub);
        module.addIncludePath(inc_path);
    }

    if (depend.lib_names.len > 0) {
        const lib_path = blk: {
            if (module.resolved_target) |target| {
                if (target.query.cpu_arch) |arch| {
                    const arch_path = module_path.path(b, @tagName(arch));
                    break :blk arch_path.path(b, "bin");
                }
            }

            break :blk module_path.path(b, "bin");
        };
        module.addLibraryPath(lib_path);

        for (depend.lib_names) |name| {
            module.linkSystemLibrary(name, .{ .use_pkg_config = .no });
        }
    }
}

fn addAwtkModule(
    b: *std.Build,
    app_name: []const u8,
    target: std.Build.ResolvedTarget,
    optimize: std.builtin.OptimizeMode,
) *std.Build.Module {
    const module = b.addModule("awtk", .{
        .root_source_file = b.path("src/awtk.zig"),
        .target = target,
        .optimize = optimize,
        .link_libc = true,
        .link_libcpp = true,
    });

    module.addIncludePath(b.path("../awtk/src/"));
    module.addIncludePath(b.path("../awtk/src/ext_widgets/"));

    const awtk_lib_dir = if (isCrossBuild(target)) "../awtk-linux-fb/bin" else "../awtk/bin";
    module.addLibraryPath(b.path(awtk_lib_dir));

    module.linkSystemLibrary("awtk", .{ .use_pkg_config = .no });

    for (dependences) |depend| {
        addDependence(b, module, depend);
    }

    const lcd_width = b.option(u16, "width", "lcd width") orelse 1024;
    const lcd_high = b.option(u16, "high", "lcd high") orelse 600;

    const awtk_options = b.addOptions();
    awtk_options.addOption(u16, "width", lcd_width);
    awtk_options.addOption(u16, "high", lcd_high);
    awtk_options.addOption([]const u8, "app_name", app_name);

    module.addOptions("config", awtk_options);

    return module;
}

// Although this function looks imperative, note that its job is to
// declaratively construct a build graph that will be executed by an external
// runner.
pub fn build(b: *std.Build) !void {
    const is_d21x = b.option(bool, "d21x", "build for MPU D21x") orelse false;

    // Standard target options allows the person running `zig build` to choose
    // what target to build for. Here we do not override the defaults, which
    // means any target is allowed, and the default is native. Other options
    // for restricting supported target set are available.
    const target = if (is_d21x)
        b.standardTargetOptions(.{ .default_target = .{
            .cpu_arch = .riscv64,
            .os_tag = .linux,
            .abi = .gnu,
            .cpu_model = .{ .explicit = std.Target.Cpu.Model.generic(.riscv64) },
            .cpu_features_add = std.Target.riscv.featureSet(&.{ .i, .m, .a, .f, .d, .c }),
        } })
    else
        b.standardTargetOptions(.{});

    // Standard optimization options allow the person running `zig build` to select
    // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
    // set a preferred release mode, allowing the user to decide how to optimize.
    const optimize = b.standardOptimizeOption(.{});

    // We will also create a module for our other entry point, 'main.zig'.
    const exe_mod = b.createModule(.{
        // `root_source_file` is the Zig "entry point" of the module. If a module
        // only contains e.g. external object files, you can make this `null`.
        // In this case the main source file is merely a path, however, in more
        // complicated build scripts, this could be a generated file.
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
        .link_libc = true,
    });

    const app_name = "awtk-mvvm-zig-example";
    const awtk = addAwtkModule(b, app_name, target, optimize);

    exe_mod.addImport("awtk", awtk);

    const exe = b.addExecutable(.{
        .name = app_name,
        .root_module = exe_mod,
    });

    for (awtk.include_dirs.items) |dir| {
        exe.addIncludePath(dir.path);
    }

    const clean_cmd = b.addSystemCommand(&.{ "rm", "-r", "./.zig-cache" });
    const clean_step = b.step("clean", "Clean build cache");
    clean_step.dependOn(&clean_cmd.step);

    // system command that builds UI resources
    const build_res_cmd = b.addSystemCommand(&.{ "python", "scripts/update_res.py" });
    const build_res_step = b.step("res", "Build UI resources, add `help` option for usage");

    // args pass to scripts/update_res.py, like this: `zig build res -- ui`, default to "all"
    if (b.args) |args| {
        if (!std.mem.eql(u8, args[0], "help")) {
            build_res_cmd.addArgs(args);
        }
    } else {
        build_res_cmd.addArg("all");
    }

    build_res_step.dependOn(&build_res_cmd.step);

    // This declares intent for the executable to be installed into the
    // standard location when the user invokes the "install" step (the default
    // step when running `zig build`).
    //     b.installArtifact(exe);
    const target_output = b.addInstallArtifact(exe, .{
        .dest_dir = .{
            .override = .{
                .custom = try target.query.zigTriple(b.allocator),
            },
        },
    });

    b.getInstallStep().dependOn(&target_output.step);

    // This *creates* a Run step in the build graph, to be executed when another
    // step is evaluated that depends on it. The next line below will establish
    // such a dependency.
    const run_cmd = b.addRunArtifact(exe);

    // By making the run step depend on the install step, it will be run from the
    // installation directory rather than directly from within the cache directory.
    // This is not necessary, however, if the application depends on other installed
    // files, this ensures they will be present and in the expected location.
    run_cmd.step.dependOn(b.getInstallStep());

    // This allows the user to pass arguments to the application in the build
    // command itself, like this: `zig build run -- arg1 arg2 etc`
    if (b.args) |args| {
        run_cmd.addArgs(args);
    }

    // This creates a build step. It will be visible in the `zig build --help` menu,
    // and can be selected like this: `zig build run`
    // This will evaluate the `run` step rather than the default, which is "install".
    const run_step = b.step("run", "Run the app");
    run_step.dependOn(&run_cmd.step);

    const exe_unit_tests = b.addTest(.{
        .root_module = exe_mod,
    });

    exe_unit_tests.root_module.addImport("awtk", awtk);
    for (awtk.include_dirs.items) |dir| {
        exe_unit_tests.addIncludePath(dir.path);
    }

    const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);

    // Similar to creating the run step earlier, this exposes a `test` step to
    // the `zig build --help` menu, providing a way for the user to request
    // running the unit tests.
    const test_step = b.step("test", "Run unit tests");
    test_step.dependOn(&run_exe_unit_tests.step);

    const check = b.step("check", "Compile check for ZLS");
    check.dependOn(&exe.step);
    check.dependOn(&exe_unit_tests.step);
}
